package de.lgohlke.helper;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.GZIPInputStream;
import java.util.zip.InflaterInputStream;

import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
import org.apache.commons.compress.compressors.bzip2.BZip2CompressorOutputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;

/**
 * <p>CompressionHelper class.</p>
 *
 * @author lars
 * @version $Id: $
 */
public final class CompressionHelper
{
  private CompressionHelper()
  {
    // ok
  }

  public enum TYPE
  {
    BZIP, GZIP,
    // LZMA, PBZIP
  }

  /**
   * <p>compress.</p>
   *
   * @param data an array of byte.
   * @return a {@link de.lgohlke.helper.CompressionHelper.CompressionTypeDefinition} object.
   */
  public static CompressionTypeDefinition compress(final byte[] data)
  {
    return new CompressionTypeDefinition(data);
  }

  /**
   * <p>uncompress.</p>
   *
   * @param data an array of byte.
   * @return a {@link de.lgohlke.helper.CompressionHelper.UnCompressionTypeDefinition} object.
   */
  public static UnCompressionTypeDefinition uncompress(final byte[] data)
  {
    return new UnCompressionTypeDefinition(data);
  }

  public static class CompressionTypeDefinition
  {
    private final byte[] data;

    public CompressionTypeDefinition(final byte[] data)
    {
      this.data = data;
    }

    public CompressionDefinition type(final TYPE type)
    {
      return new CompressionDefinition(data, type);
    }
  }

  public static class UnCompressionTypeDefinition
  {
    private final byte[] data;

    public UnCompressionTypeDefinition(final byte[] data)
    {
      this.data = data;
    }

    public DecompressionDefinition type(final TYPE type)
    {
      return new DecompressionDefinition(data, type);
    }
  }

  public static class DecompressionDefinition
  {
    private final ByteArrayInputStream inputstream;
    private final TYPE                 type;

    public DecompressionDefinition(final byte[] data, final TYPE type)
    {
      this.inputstream = new ByteArrayInputStream(data);
      this.type = type;
    }

    public byte[] getBytes() throws IOException
    {
      ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
      Writer writer = new RawWriter(outputStream);

      Reader reader = null;

      switch (type)
      {
        case BZIP:
          reader = new CompressionHelper.BZIP2Reader(inputstream);
          break;
        // case PBZIP:
        // reader = new CompressionHelper.BZIP2ReaderParallel(inputstream);
        // break;
        case GZIP:
          reader = new CompressionHelper.GZIPReader(inputstream);
          break;
        // case LZMA:
        // reader = new CompressionHelper.LzmaReader(inputstream);
        // break;
        default:
          throw new IOException("type not supported : " + type);
      }

      convertStream(writer, reader);

      return outputStream.toByteArray();
    }
  }

  public static class CompressionDefinition
  {
    private int                        level = -1;
    private final ByteArrayInputStream inputstream;
    private final TYPE                 type;

    public CompressionDefinition(final byte[] data, final TYPE type)
    {
      this.inputstream = new ByteArrayInputStream(data);
      this.type = type;
    }

    public CompressionDefinition level(final int level)
    {
      this.level = level;
      return this;
    }

    public byte[] getBytes() throws IOException
    {
      RawReader reader = new RawReader(inputstream);
      ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
      Writer writer = null;

      switch (type)
      {
        case BZIP:
          writer = new CompressionHelper.BZIP2Writer(outputStream, level);
          break;
        // case PBZIP:
        // writer = new CompressionHelper.BZIP2WriterParallel(outputStream, level);
        // break;
        case GZIP:
          writer = new CompressionHelper.GZIPWriter(outputStream);
          break;
        // case LZMA:
        // writer = new CompressionHelper.LzmaWriter(outputStream);
        // break;
        default:
          throw new IOException("type not supported : " + type);
      }

      convertStream(writer, reader);

      return outputStream.toByteArray();
    }
  }

  interface StreamReaderWriter
  {
    void close() throws IOException;
  }

  interface StreamWriter extends StreamReaderWriter
  {
    void write(byte[] buf, int off, int len) throws IOException;
  }

  interface StreamReader extends StreamReaderWriter
  {
    int read(byte[] buf) throws IOException;
  }

  abstract static class ReaderWriter<T> implements StreamReaderWriter
  {
    private T               stream;
    public final static int bufSize = 1024 * 10;

    public void setStream(final T stream)
    {
      this.stream = stream;
    }

    public T getStream()
    {
      return stream;
    }

  }

  abstract static class Reader extends ReaderWriter<InputStream> implements StreamReader
  {
    public void close() throws IOException
    {
      getStream().close();
    }

    /*
     * (non-Javadoc)
     * @see org.apache.log4j.DailyRollingCompressingFileAppender.CompressionWriter#write(byte[], int, int)
     */
    public final int read(final byte[] buf) throws IOException
    {
      return getStream().read(buf);
    }
  }

  public abstract static class Writer extends ReaderWriter<OutputStream> implements StreamWriter
  {
    public void close() throws IOException
    {
      getStream().close();
    }

    public void write(final byte[] buf, final int off, final int len) throws IOException
    {
      getStream().write(buf, off, len);
    }
  }

  public static class GZIPWriter extends Writer
  {
    public GZIPWriter(final OutputStream destination) throws IOException
    {
      setStream(new GzipCompressorOutputStream(destination));
    }

    public GZIPWriter(final OutputStream destination, final int level) throws IOException
    {
      this(destination);
    }
  }

  public static class BZIP2Writer extends Writer
  {
    private final static int DEFAULT_LEVEL = 6;

    public BZIP2Writer(final OutputStream destination) throws IOException
    {
      this(destination, DEFAULT_LEVEL);
    }

    public BZIP2Writer(final OutputStream destination, final int _level) throws IOException
    {
      int level = _level;
      if ((level < 0) || (level > 9))
      {
        level = DEFAULT_LEVEL;
      }
      setStream(new BZip2CompressorOutputStream(destination, level));
    }
  }

  // public static class BZIP2WriterParallel extends Writer
  // {
  // private final static int DEFAULT_LEVEL = 6;
  // private final BZip2EncoderExecutorService executor;
  //
  // public BZIP2WriterParallel(OutputStream destination) throws IOException
  // {
  // this(destination, DEFAULT_LEVEL);
  // }
  //
  // public BZIP2WriterParallel(OutputStream destination, int level) throws IOException
  // {
  // if (level < 0 || level > 9)
  // {
  // level = DEFAULT_LEVEL;
  // }
  //
  // // Create a BZip2EncoderExecutorService with four threads.
  // executor = BZip2OutputStream.createExecutorService(Runtime.getRuntime().availableProcessors());
  //
  // // A settings object containing the executor service
  // BZip2OutputStreamSettings settings = new BZip2OutputStreamSettings().setExecutorService(executor);
  // settings.setBlockSize(level);
  // settings.setNumberOfHuffmanTreeRefinementIterations(20);
  //
  // setStream(new BZip2OutputStream(destination, settings));
  // }
  //
  // @Override
  // public void close() throws IOException
  // {
  // super.close();
  // executor.shutdown();
  // }
  // }

  // public static class LzmaWriter extends Writer
  // {
  // private final static int DEFAULT_LEVEL = 6;
  //
  // public LzmaWriter(OutputStream destination) throws IOException
  // {
  // this(destination, DEFAULT_LEVEL);
  // }
  //
  // public LzmaWriter(OutputStream destination, int level) throws IOException
  // {
  // if (level < 0 || level > 9)
  // {
  // level = DEFAULT_LEVEL;
  // }
  //
  // // removed params , 31, 10
  // setStream(new LzmaOutputStream(destination));
  // }
  // }

  public static class RawWriter extends Writer
  {
    public RawWriter(final OutputStream destination) throws IOException
    {
      setStream(destination);
    }
  }

  public static class DeflateWriter extends Writer
  {

    public DeflateWriter(final OutputStream destination) throws IOException
    {
      Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION);
      setStream(new DeflaterOutputStream(destination, deflater, bufSize));
    }
  }

  public static class GZIPReader extends Reader
  {
    public GZIPReader(final InputStream src) throws IOException
    {
      setStream(new GZIPInputStream(src, bufSize));
    }
  }

  public static class BZIP2Reader extends Reader
  {
    public BZIP2Reader(final InputStream source) throws IOException
    {
      setStream(new BZip2CompressorInputStream(source));
    }
  }

  public static class BZIP2ReaderParallel extends Reader
  {
    public BZIP2ReaderParallel(final InputStream source) throws IOException
    {
      setStream(new BZip2CompressorInputStream(source));
    }
  }

  // public static class LzmaReader extends Reader
  // {
  // public LzmaReader(InputStream source) throws IOException
  // {
  // setStream(new LzmaInputStream(source));
  // }
  // }

  public static class DeflateReader extends Reader
  {
    public DeflateReader(final InputStream src) throws IOException
    {
      setStream(new InflaterInputStream(src));
    }
  }

  public static class RawReader extends Reader
  {
    public RawReader(final InputStream src) throws IOException
    {
      setStream(src);
    }
  }

  /**
   * <p>convertStream.</p>
   *
   * @param writer a {@link de.lgohlke.helper.CompressionHelper.StreamWriter} object.
   * @param reader a {@link de.lgohlke.helper.CompressionHelper.StreamReader} object.
   * @throws java.io.IOException if any.
   */
  public static void convertStream(final StreamWriter writer, final StreamReader reader) throws IOException
  {

    byte[] buf = new byte[ReaderWriter.bufSize];
    int len;

    while ((len = reader.read(buf)) > 0)
    {
      writer.write(buf, 0, len);
    }

    reader.close();
    writer.close();
  }
}
